const axios = require("axios").default.create({
  headers: {
    "User-Agent": "TuNiuApp/9.51.0 (Android)"
  }
});
const qs = require("querystring");
const rannum = require("random-number");
const js_base64_base64 = require("js-base64").Base64;
js_base64_base64.extendString();
const util = require("util");
const path = require("path");
const fs = require("fs");
const uuid = require("uuid");
const url = require("url");
const FormData = require("form-data");
const buffer = require("buffer");
const http = require("http");
const crypto = require("crypto");
const filesize = require("filesize");
const Toolbox = require("../Toolbox");

const CommonAxerrHandlerGen = (resolve) => (axerr) => {
  if (axerr.response) {
    resolve({
      ok: false,
      msg: `HTTP ${axerr.response.status} ${axerr.response.statusText} : ${axerr.response.data ?
        (axerr.response.data['message'] ? axerr.response.data['message'] : util.inspect(axerr.response.data)) :
        "!NO HTTP RESPONSE DATA"}`
    })
  } else {
    resolve({
      ok: false,
      msg: axerr.message ? axerr.message : util.inspect(axerr)
    });
  }
};




const TuniuApp = class Tuniu {
  constructor() {

    this.sessionid = "";

    /**@type {(full_path:string,parts_count:number,part_index:number)=>string} */
    this.default_part_name_func = (full_path, parts_count, part_index) => {
      parts_count = parseInt(parts_count);
      if (parts_count == 1) {
        return path.basename(full_path);
      } else {
        let max_length = String(parts_count).length;
        let str_index = String(part_index);
        while (str_index.length < max_length) {
          str_index = `0${str_index}`
        }
        return `${path.basename(full_path)}.sf-part${str_index}`
      }
    }
  }

  /**
   *
   *
   * @returns {Promise<{ok:Boolean,msg:String}>}
   * @memberof PiankeOwhat
   */
  verify() {
    return new Promise(resolve => {
      axios.get(`https://m.tuniu.com/iapi/appserver/view/myAccountInfoV400?${JSON.stringify({
        "sessionID": this.sessionid,
        "r": Date.now(),
        "partner": 10592, "clientType": 20, "deviceType": 1, "version": "9.51.0", "_currentCityCode": "2500"
      }
      ).toBase64(false)}`, {
          headers: {
            sid: uuid(),
            sessionid: this.sessionid
          }
        }).then(axresp => {
          if (axresp.data && util.isString(axresp.data)) {
            /**@type {string} */
            let s = axresp.data;
            let oristr = js_base64_base64.decode(s);
            try {
              let j = JSON.parse(oristr);
              if (j.success === true && j.data && j.data.starDesc) {
                return resolve({
                  ok: true,
                  msg: "ok"
                })
              } else {
                throw new Error(`axresp.data decoded is ${oristr}`)
              }
            } catch (e) {
              throw new Error(`axresp.data decoded is ${oristr}`)
            }
            debugger
          } else {
            throw new Error(`axresp.data is ${util.inspect(axresp.data)}`)
          }
        }).catch(axerr => {
          CommonAxerrHandlerGen(axerr);
        })
    })
  }



  /**
   * 
   * @param {import("fs").ReadStream|String} stream 
   * @param {Strings} define_filename 
   * @returns {Promise<{ok:Boolean,msg:String,url:String}>}
   */
  uploadStream(stream, define_filename = "test.bin") {
    const upload_url = "https://m.tuniu.com/api/trip/trip/writeTripUploadPic";
    const parsed = url.parse(upload_url);
    return new Promise(async resolve => {
      let ext = path.extname(define_filename);
      ext = ext.replace(".", "")
      let form = new FormData;
      form.append("sessionId", this.sessionid);
      form.append("photos[]", stream, {
        filename: `${Math.random()}.${ext}`
      });
      let headers = {
        "User-Agent": `TuNiuApp/9.51.0 (Android)`,
      };
      let request = http.request({
        method: "POST",
        host: parsed.host,
        path: parsed.path,
        headers: {
          ...form.getHeaders(),
          ...headers
        }
      });
      form.on("error", err => {
        resolve({
          ok: false,
          msg: `form.on(error):${err.message}`,
          url: ""
        })
      });
      form.pipe(request);
      let response_flag = false;
      form.on("end", () => {
        setTimeout(() => {
          if (!response_flag) {
            resolve({
              ok: false,
              msg: `[LATE RESPONSE] NO HTTP RESPONSE after form.end 5 minutes`
            })
          }
        }, 60 * 5 * 1000);
      })
      request.on("response", (resp) => {
        response_flag = true;
        let buf = new buffer.Buffer("");
        resp.on("data", chunk => {
          buf += chunk;
        })
        resp.on("end", $ => {
          try {
            let obj = JSON.parse(buf);
            // debugger
            if (obj.success === true && util.isArray(obj.data) && obj.data[0] && obj.data[0].picUrl) {
              let file_url = obj.data[0].picUrl;
              return resolve({
                ok: true,
                msg: "ok",
                url: file_url
              })
            } else {
              CommonAxerrHandlerGen(resolve)(`response is not sanity:${util.inspect(obj)}`);
            }
          } catch (e) {
            resolve({
              ok: false,
              url: "",
              msg: `error when try extract url :${e.message ? e.message : util.inspect(e)};buf is ${util.inspect(buf.toString())}`
            })
          }
        })
      });
      request.on("error", err => {
        resolve({
          ok: false,
          msg: `request.on(error):${err.message}`,
          url: ""
        })
      });
    })
  }

  /**
   * @return {Promise<{ok:Boolean,msg:String,data:{}}>}
   */
  getImageToken(){
    return new Promise(resolve=>{
      /**@todo */
      axios.get(``)
    })
  }


  /**
   *  @returns {Promise<{ok:Boolean,url:String,msg:String,file_size:String,speed:String}>}
   * @param {String} full_path 
   */
  getUploadSpeed(full_path) {
    return new Promise(async resolve => {
      let o_stats = await Toolbox.getStats(full_path);
      if ((!o_stats.ok) || (!o_stats.stats.isFile())) {
        return resolve({
          ok: false,
          msg: o_stats.msg ? o_stats.msg : `${full_path} is NOT File`
        })
      }
      let start_time = Date.now();
      console.log("start uploading...", path.basename(full_path));
      let o_up = await this.uploadStream(fs.createReadStream(full_path));
      let end_time = Date.now();
      if (!o_up) {
        return resolve({
          ok: false,
          msg: o_up.msg,
        })
      } else {
        let bytes_per_second = o_stats.stats.size / ((end_time - start_time) / 1000)
        resolve({
          ok: true,
          msg: "ok",
          url: o_up.url,
          file_size: filesize(o_stats.stats.size),
          speed: filesize(bytes_per_second) + "/s"
        })
      }

    })
  }

  /**
   *
   * @returns {Promise<{ok:Boolean,msg:String,files:Array<{name:String,url:String}>}>}
   * @param {String} full_file_path
   * @param {(full_path:string,parts_count:number,part_index:number)=>string} part_name_func 
   * @param {number} [size_limit=1000*1024*1024]
   * @memberof Pianke7Niu
   */
  uploadFile(full_file_path, size_limit = 249 * 1024 * 1024, part_name_func = this.default_part_name_func) {
    size_limit = parseInt(size_limit);
    if (size_limit <= 1 || size_limit > 249 * 1024 * 1024) {
      size_limit = 249 * 1024 * 1024;
    }
    return new Promise(async resolve => {
      // debugger
      let o_stats = await Toolbox.getStats(full_file_path);
      if (!o_stats.ok) {
        return resolve({
          ok: false,
          msg: `fail to get stats of ${file_full_path} : ${o_stats.msg}`
        })
      }
      if (!o_stats.stats.isFile()) {
        return resolve({
          ok: false,
          msg: ` ${file_full_path} : it is not a file`
        })
      }
      let CHUNK_MAX_SIZE = size_limit;
      let CHUNKS_COUNT = Math.ceil(o_stats.stats.size / CHUNK_MAX_SIZE);
      /**@type {Array<{name:String,url:String}>} */
      let files = [];
      for (let i = 0; i < CHUNKS_COUNT; i++) {
        let start = i * CHUNK_MAX_SIZE;
        let end = start + CHUNK_MAX_SIZE - 1;
        if (end >= o_stats.stats.size) {
          end = o_stats.stats.size - 1;
        }
        let o_upChunk = await this.uploadStream(
          fs.createReadStream(full_file_path, {
            start: start,
            end: end
          }),
          part_name_func(full_file_path, CHUNKS_COUNT, i + 1)
        )
        if (!o_upChunk.ok) {
          return resolve({
            ok: false,
            msg: `error when upload part ${i + 1}:${o_upChunk.msg}`
          })
        }
        files.push({
          name: part_name_func(full_file_path, CHUNKS_COUNT, i + 1),
          url: o_upChunk.url
        })
      }
      resolve({
        ok: true,
        msg: "ok",
        files: files
      })
    })
  }

  /**
   *
   * @returns {Promise<{ok:Boolean,errors:string[],files:Array<{path:string,chunks:Array<{url:string,name:string}>  }>,toHTML:()=>string   }>}
   * @param {String} full_path_of_dir
   * @param {number} [size_limit=1000 * 1024 * 1024]
   * @param {String} base_dir
   * @param {(full_path:string,parts_count:number,part_index:number)=>string} [part_name_func=this.default_part_name_func]
   * @memberof Pianke7Niu
   */
  safelyUploadDir(full_path_of_dir, size_limit = 1000 * 1024 * 1024, base_dir = full_path_of_dir, part_name_func = this.default_part_name_func) {
    return new Promise(async resolve => {
      let o_stats = await Toolbox.getStats(full_path_of_dir);
      if (!o_stats.ok) {
        let errors = [`fail to get stats of ${full_path_of_dir} : ${o_stats.msg}`]
        return resolve({
          ok: false,
          errors: errors,
          toHTML: function () {
            return errors.map(e => `<font color="red">${e}</font>`).join("<br><hr>")
          }
        })
      }
      if (!o_stats.stats.isDirectory()) {
        let errors = [` ${full_path_of_dir} : it is not a folder`]
        return resolve({
          ok: false,
          errors: errors,
          toHTML: () => errors.map(e => `<font color="red">${e}</font>`).join("<br><hr>")
        })
      }
      let safeList = await Toolbox.safeListDir(full_path_of_dir);
      /**@type {string[]} */
      let errors = [];
      /**@type {Array<{path:string,chunks:Array<{url:string,name:string}> } >} */
      let files = [];
      for (let item of safeList) {
        if (item.stats.isDirectory()) {
          let o_uploadChildFolder = await this.safelyUploadDir(item.full_path, size_limit, base_dir, part_name_func);
          errors = errors.concat(o_uploadChildFolder.errors);
          files = files.concat(o_uploadChildFolder.files);
        } else if (item.stats.isFile()) {
          let o_upFile = await this.uploadFile(item.full_path, size_limit, part_name_func);
          if (!o_upFile.ok) {
            errors.push(`failed to upload ${path.relative(base_dir, item.full_path)}:${o_upFile.msg}`)
          } else {
            files.push({
              path: path.relative(base_dir, item.full_path),
              chunks: o_upFile.files.map(o => {
                return {
                  url: o.url,
                  name: o.name
                }
              })
            })
          }
        }
      }
      resolve({
        ok: true,
        errors: errors,
        files: files,
        toHTML: function () {
          let error_html = errors.map(e => `<font color="#bf341c">${e}</font>`).join("<br><hr>");
          let files_html = files.map(file => {
            if (file.chunks.length == 1) {
              return `<a href="${file.chunks[0].url}"><font color="#218a21"><u>${file.path}</u></font></a>`
            } else {
              return file.chunks.map((chunk, i, chunks) => `<a href="${chunk.url}"><font color="#218a21"><u>${
                path.join(path.dirname(file.path), chunk.name)
                }</u></font></a>`).join("<br>")
            }
          }).join("<br>");
          return `${error_html ? `<blockquote><h4><font color="#bf341c">ERROR LOGS : </font></h4>${error_html}</blockquote>` : ""}` +
            `<blockquote><h4><font color="#5cda5c">OK LINKS : </font></h4><p><blockquote>${files_html}</blockquote></p></blockquote>`
        }
      })

    })
  }
};

const InstancePoll = {
  "-": new TuniuApp
};

class TuniuWrapper {

  /**
   *
   *
   * @static
   * @returns {Promise<{ok:Boolean,msg:String,sessionid:String}>}
   * @memberof TuniuWrapper
   */
  static __getSessionId() {
    return new Promise(resolve => {
      let imei = `8677870360752${rannum({
        min: 10,
        max: 65
      })}`
      axios.post(`https://api.tuniu.com/auth/beginSession?c=%7B%22cc%22%3A2500%2C%22ct%22%3A20%2C%22dt%22%3A1%2C%22ov%22%3A20%2C%22p%22%3A10593%2C%22v%22%3A%229.51.0%22%7D`, {
        "activateTimes": 0,
        "parameters": {
          "apiType": 1,
          "clientType": 20,
          "createTime": ((new Date).toUTCString()) || "Nov 22, 2018 13:31:15", //时间格式不一样居然也行
          "deviceType": 1,
          "imei": imei || "867787036075244", //填了假的居然也行
          "lg": Date.now() || "1542864675946",
          "partner": 10590 || 10593, //感觉是随便填都行
          "sid": `7015298869125210817602122918413312138${rannum({ min: 15, max: 24 })}` || "701529886912521081760212291841331213824", //填了假的居然也行
          "token": imei || "867787036075244", //填了假的居然也行
          "version": "9.51.0"
        },
        "sessionId": "0"
      }, {
          headers: {
            'Content-Type': 'application/json; charset=UTF-8',
            sid: uuid()
          }
        }).then(axresp => {
          if (axresp.data && axresp.data.success === true && axresp.data.data && axresp.data.data.sessionId) {
            resolve({
              ok: true,
              msg: "ok",
              sessionid: axresp.data.data.sessionId
            })
          } else {
            throw new Error(`response is not sanity:${util.inspect(axresp.data)}`)
          }
        }).catch(axerr => {
          CommonAxerrHandlerGen(resolve)(axerr);
        })
    })
  }


  /**
   * @returns {Promise<{ok:Boolean,msg:String,instance:TuniuApp }>}
   * @param {String} loginid 
   * @param {String} password 
   */
  static __login(loginid = "18243877534", password = "tuniutest414") {
    return new Promise(async resolve => {
      let o_session = await this.__getSessionId();
      if (!o_session.ok) {
        return resolve({
          ok: false,
          msg: `error when get session id:${o_session.msg}`,
          instance: null
        })
      }
      let client_uuid = uuid();
      let md5_password = crypto.createHash("md5").update(password).digest("hex");
      axios.post(`https://m.tuniu.com/api/user/auth/login`, "", {
        params: {
          d: JSON.stringify({
            captcha: "",
            deviceId: `hIYYke0cCIqCMaPwQ19PM-ZGYyrJLHi41gMo5gDbNCInYEejWmkrILe9pTFXdNPc3zvfpvopC8AwM4bTf0B2cDm_ja9DSyulqIucukEp25S2VWI3N86AiNAWeTWsZIuf_JpyC1uvC4X0mW7nhnnfxTsihiL5CvhS`,
            isDynamic: 0,
            loginId: loginid,
            password: md5_password,
            sessionId: o_session.sessionid
          }),
          c: JSON.stringify({
            "cc": 2500,
            "ct": 20,
            "dt": 1,
            "ov": 20,
            "p": 10593,
            "v": "9.51.0"
          })
        },
        headers: {
          sid: uuid(),
          sessionid: o_session.sessionid
        },
      }).then(axresp => {
        if (axresp.data && axresp.data.success === true && axresp.data.data && axresp.data.data.intlCode) {
          let tuniu = new TuniuApp;
          tuniu.sessionid = o_session.sessionid
          resolve({
            ok: true,
            msg: "ok",
            instance: tuniu
          })
        } else {
          throw new Error(util.inspect(axresp.data));
        }
      }).catch(axerr => {
        CommonAxerrHandlerGen(resolve)(axerr);
      })
    })
  }

  /**
   * @returns {Promise<{ok:Boolean,msg:String,instance:TuniuApp }>}
   * @param {String} loginid phone or unique nickname
   * @param {String} password 
   */
  static getValidInstance(loginid = "18243877534", password = "tuniutest414") {
    let key = `${loginid}:${password}`;
    return new Promise(async resolve => {
      if (!!InstancePoll[key]) {
        /**@type {TuniuApp} */
        let ins = InstancePoll[key];
        let o_verify = await ins.verify();
        if (o_verify.ok) {
          resolve({
            ok: true,
            msg: "ok",
            instance: ins
          });
        } else {
          InstancePoll[key] = null;
          let o_retry = await this.getValidInstance(loginid, password);
          resolve(o_retry);
        }
      } else {
        let o_login = await this.__login(loginid, password);
        if (o_login.ok) {
          InstancePoll[key] = o_login.instance;
          resolve({
            ok: true,
            msg: "ok",
            instance: o_login.instance
          });
        } else {
          CommonAxerrHandlerGen(resolve)(`this.__login : ${o_login.msg}`);
        }
      }
    });
  }


}

TuniuWrapper.TuniuClass = TuniuApp;

module.exports = {
  TuniuWrapper: TuniuWrapper
}